iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
自我挑戰組

30 天 hypervisor 入門系列 第 20

Day 20 實現螢幕設備(1)

  • 分享至 

  • xImage
  •  

在 x86 BIOS 環境保留了固定的地址作為畫面顯示使用(0Xb8000) 則對應彩色的文本寫入 (80 * 25),而 0xA0000 則用於圖形模式顯式。而現代的設備的 framebuffer 地址會透過 VBE/UEFI 提供另外這個地址式可配置的,不過為了簡單起見這裡我們先寫死,先實現 0xB8000 的文本顯示。

螢幕顯示

由於顯示設備通常以 MMIO(Memory-Mapped I/O) 方式把畫面緩衝區 (frame buffer) 暴露給 CPU,對 CPU 而言更新畫面只是向某個地址寫入記憶體而已。因此,如果這段位址被註冊為一般的 guest RAM,guest 的寫入不會觸發 VM-exit,我們的 exit handler 就無法直接捕捉這類事件。要想在 host 端觀察到 framebuffer 的變更,以下提供簡單的觀察方法:

1.把那段地址當作 MMIO(不註冊為 RAM)或用 EPT 寫保護來強制 VM-exit
如果不把那段 GPA 註冊到 KVM 做為 RAM 使用,任何訪問會被視為 MMIO,讓 KVM 產生 KVM_EXIT_MMIO 的 VM-exit。或者我們可透過把頁的寫入權限清除(即 read-only),讓 Guest 寫入時觸發 EPT violation 並產生 VM-exit 讓 Host 可以即時處理。
但是頻繁的像素更新會造成大量的 VM-exit 這將造成大量的性能損耗。

2.在 Host 建立 timer 定期輪詢並刷新 guest frame buffer

把 framebuffer 當作正常的 guest RAM 映射回 host,host 以固定時間間隔 (如 10–50 ms) 取樣該記憶體區塊即可,這種做法簡單甚至可天然的透過 DMA 去做頁面處理。
由於 KVM 在建立 memory slot 時支援 dirty logging,因此我們可把方法 2 進一步改成差量更新,每次刷新前只需向 KVM 確認哪些頁被 Guest 修改過,讓 Host 的任務從刷新整個畫面變成刷新被修改頁的局部更新。

在先前的實作中,我們以 epoll 為核心建立了 host I/O 模組,我們可以簡單地將 timerfd 加入到 host I/O 模組中,並將畫面更新的邏輯掛接到處理方法中。這樣就可以在已有的 I/O thread 中定期處理畫面更新。

struct screen {
    struct host_io *io; // epoll 封裝的設備管理結構
    struct host_io_handle *timer_handle; // 用 timer 定期觸發
    struct device_timer timer; // timer 結構體紀錄包括 fd 等信息
    uint16_t *cells; // frame buffer
    uint64_t *dirty_bitmap; // 用來取 dirty log
    size_t dirty_bitmap_words;
    size_t cell_count;
    size_t columns;
    size_t rows;
    size_t page_start;
    size_t page_end;
    uint32_t slot_id; // 監控的 frame buffer 對應的 memory slot 編號
    int vm_fd;
    int refresh_interval_ms; // 螢幕刷新周期
};

int screen_init(struct screen *screen,
                struct host_io *io,
                int refresh_interval_ms)
{
    if (!screen || !io || refresh_interval_ms <= 0) {
        errno = EINVAL;
        return -1;
    }

    memset(screen, 0, sizeof(*screen));
    screen->io = io;
    screen->refresh_interval_ms = refresh_interval_ms;

    if (device_timer_init_ms(&screen->timer, refresh_interval_ms) < 0) // 建立 tiemrfd
        goto fail;

    screen->timer_handle = host_io_register(io, screen->timer.fd,
                                            EPOLLIN | EPOLLERR | EPOLLHUP,
                                            screen_on_timer, screen); // 註冊到 host io
    if (!screen->timer_handle) {
        int saved = errno;
        device_timer_destroy(&screen->timer);
        screen->io = NULL;
        screen->refresh_interval_ms = 0;
        errno = saved;
        return -1;
    }

    return 0;

fail:
    screen->io = NULL;
    screen->refresh_interval_ms = 0;
    screen->timer_handle = NULL;
    return -1;
}

這樣就完成透過 tiemrfd 定期觸發螢幕刷新事件。

static void screen_on_timer(int fd, uint32_t events, void *opaque)
{
    struct screen *screen = opaque;
    if (!screen)
        return;

    if ((events & (EPOLLERR | EPOLLHUP)) != 0) {
        perror("screen timerfd event");
        return;
    }

    if ((events & EPOLLIN) == 0)
        return;

    uint64_t expirations = 0;

    for (;;) {
        ssize_t n = read(fd, &expirations, sizeof(expirations));
        if (n == (ssize_t)sizeof(expirations))
            break;
        if (n < 0 && errno == EINTR)
            continue;
        if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
            return;
        perror("screen timerfd read");
        return;
    }

    if (expirations == 0 || !screen->cells || screen->cell_count == 0)
        return;

    screen_refresh(screen);
}

這個處理函數只做兩件事,用 read 把緩衝區清空,同時為了避免緩衝被打斷,用 for (;;) 刷新直到確定刷完為止。處理完後就謮轉發到 screen_refresh 做真正的處理邏輯。

void screen_refresh(struct screen *screen)
{
    if (!screen || !screen->cells || screen->columns == 0 || screen->rows == 0)
        return;

    if (screen->cell_count == 0)
        return;

    if (screen->use_dirty_log)
        screen_update_dirty_bitmap(screen);

    screen_render_full(screen);
}

這裡就是真的刷新邏輯。


上一篇
Day 19 實現 INT 13h 磁碟功能
下一篇
Day 21:BIOS 顯示服務(INT 10h)
系列文
30 天 hypervisor 入門23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言